<?php

/**
 * @copyright Michiel Hakvoort 2010
 * @license http://www.opensource.org/licenses/bsd-license.php New BSD
 * @package mangrove
 * @subpackage core
 * @filesource
 */

/*
 * Copyright (c) 2010 Michiel Hakvoort
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */

namespace mg;

use Closure;

define('DS', DIRECTORY_SEPARATOR);
define('MG_VERSION', '0.1');

include(__DIR__ . DS . 'BootstrapGuard.php');
include(__DIR__ . DS . 'ClassLoader.php');
include(__DIR__ . DS . 'Context.php');
include(__DIR__ . DS . 'Exception.php');
include(__DIR__ . DS . 'EngineException.php');
include(__DIR__ . DS . 'FileSystem.php');
include(__DIR__ . DS . 'Logger.php');
include(__DIR__ . DS . 'architecture' . DS . 'Architecture.php');
include(__DIR__ . DS . 'architecture' . DS . 'ClassSource.php');
include(__DIR__ . DS . 'architecture' . DS . 'Tokenizer.php');
include(__DIR__ . DS . 'cache' . DS . 'CacheProvider.php');
include(__DIR__ . DS . 'storage' . DS . 'StorageManager.php');
include(__DIR__ . DS . 'util' . DS . 'G11N.php');
include(__DIR__ . DS . 'util' . DS . 'Util.php');

/**
 * The Mangrove Runtime is the central class which is responsible
 * for bootstrapping the framework. The Runtime must be configured
 * with a writable folder on the file system, in order to store
 * relevant information acquired in the build process.
 *
 * @author Michiel Hakvoort <michiel@hakvoort.it>
 *
 */
final class Runtime implements Transactional {

    const MODE_DEVELOPMENT	= 0x00;
    const MODE_TESTING		= 0x01;
    const MODE_ACCEPTANCE	= 0x02;
    const MODE_PRODUCTION	= 0x03;

    const OS_WIN		= 0x00;
    const OS_LINUX		= 0x01;
    const OS_BSD		= 0x02;
    const OS_UNKNOWN	= 0x03;

    private static $os = null;

    private $root = null;

    private $bootstrapGuard = null;

    private $mode = null;

    private $classSources = array();

    private $token = null;

    private $isBootstrapped = false;

    private $isTransactional = false;

    private $resources = array();

    private $transactionals = array();

    private $lHandle = null;

    /**
     * The ContextConfiguration used in the Runtime.
     *
     * @var ContextConfiguration
     */
    private $runtimeConfiguration = null;

    private $version = null;

    /**
     * Instantiate the Runtime for the given root folder.
     *
     * All files in this root folder should be considered volatile,
     * as the Runtime might remove any files in this folder during
     * the build process without prior warning.
     *
     * @param string $root The root folder of the Runtime
     */
    public function __construct($root) {
        $this->root = $root;
    }

    /**
     * Add a {@link \mg\ClassSource} which should be considered
     * as a source for PHP classes. During the build process all
     * ClassSources are queried for creating a taxonomy of all
     * available classes.
     *
     * @param ClassSource $classSource
     *
     * @return void
     */
    public function addClassSource(ClassSource $classSource) {
        $this->classSources[] = $classSource;
    }

    /**
     * Set the {@link \mg\ContextConfiguration} which should be used
     * in an execution of this Runtime. Setting the runtime configuration
     * fills the initial execution context with the set configuration.
     *
     * @param \mg\ContextConfiguration $runtimeConfiguration The default ContextConfiguration
     *
     * @return void
     */
    public function setRuntimeConfiguration(ContextConfiguration $runtimeConfiguration) {
        $this->runtimeConfiguration = $runtimeConfiguration;
    }

    /**
     * Return the active Runtime mode. This can be any of {@link \mg\Runtime :: MODE_DEVELOPMENT},
     * {@link \mg\Runtime :: MODE_TESTING}, {@link \mg\Runtime :: MODE_ACCEPTANCE} or {@link \mg\Runtime :: MODE_PRODUCTION}.
     *
     * By default, the Runtime runs in development mode. Changing the mode is possible through changing
     * the mode setting. In order to assure multiple Runtimes with the same root run in the same mode,
     * the mode is set and stored in the mode file, named "mode.php" and located in the Runtime's root
     * folder.
     *
     * @return int Any of the available Runtime modes
     */
    public function getMode() {
        return $this->mode;
    }

    /**
     * Set the {@link \mg\BootstrapGuard} which will be used when the framework
     * is being deployed in a production or acceptance environment. Prior to
     * deployment the BootstrapGuard is queried on whether deployment is allowed.
     * The goal of this ability to restrict deployment is to control the  actual
     * deployment of the framework.
     *
     * @param BootstrapGuard $bootstrapGuard The to be used BootstrapGuard
     *
     * @return void
     */
    public function setBootstrapGuard(BootstrapGuard $bootstrapGuard) {
        $this->bootstrapGuard = $bootstrapGuard;
    }

    /**
     * Execute the given anonymous function within the context of the
     * Mangrove framework. Before execution the environment will be build
     * if required. During execution of the anonymous function, the timezone
     * and mbstring internal character set will be set to, respectively,
     * 'UTC' and 'UTF-8'. Next to these property changes, class loading
     * is now handled by the Mangrove class loader and any triggered errors
     * are turned into {@link \mg\EngineException EngineExceptions}
     *
     * @param Closure $closure
     *
     * @throws \mg\Exception
     *
     * @return void
     */
    public function __invoke(Closure $closure) {
        // deployment phase

        // TODO improve performance (skip realpath + is_* checks)
        // Perhaps make production mode fallback into dev. mode (no single check on run, but an error retries to build in dev mode)

        $root = realpath($this->root);

        if($root === false) {
            throw new Exception("'{$this->root}' does not exist");
        } elseif(!is_dir($root)) {
            throw new Exception("'{$root}' is not a directory");
        } elseif(!is_writable($root)) {
            throw new Exception("'{$root}' is not a writable directory");
        } elseif($this->getOS() !== self :: OS_WIN && !is_executable($root)) {
            throw new Exception("'{$root}' is not a writable directory with executable rights, cannot write to directory");
        }

        $this->root = $root;

        $mFilename = $this->root . DS . 'mode.php';
        $dFilename = $this->root . DS . 'deployed.mg';
        $lFilename = $this->root . DS . 'volatile' . DS . 'lock.mg';

        $lHandle = null;

        /*
         * Extract (or set) the mode from the mode file in the root folder
         */
        $this->mode = @include($mFilename);

        if($this->mode === false) {
            touch($mFilename);
            chmod($mFilename, 0666);
            file_put_contents($mFilename, '<?php return \mg\Runtime :: MODE_DEVELOPMENT;', FILE_TEXT);

            $this->mode = self :: MODE_DEVELOPMENT;
        }

        switch($this->mode) {
            case self :: MODE_DEVELOPMENT : {
                if(file_exists($dFilename)) {

                    if(is_dir($dFilename)) {
                        throw new Exception("Invalid runtime state : '{$dFilename}' is a directory");
                    } elseif(!is_writable($dFilename)) {
                        throw new Exception("Invalid runtime state : '{$dFilename}' is not writable");
                    }

                    unlink($dFilename);
                }

                $this->isTransactional = true;
            }

            case self :: MODE_TESTING :
            case self :: MODE_PRODUCTION :
            case self :: MODE_ACCEPTANCE : {

                // If the runtime is already deployed, do not redeploy
                if(file_exists($dFilename)) {
                    break;
                }

                // In case of production, guard the deployment
                if($this->mode === self :: MODE_PRODUCTION) {
                    if($this->bootstrapGuard !== null && $this->bootstrapGuard->guard()) {
                        return;
                    }
                }

                $this->lock();

                $this->version = $this->readVersion();

                /*
                 * During the time waiting to acquire a lock, the runtime may be deployed.
                 * If this is the case, do not re-deploy
                 */

                $isTransactional = !file_exists($dFilename);

                $this->unlock();

                $this->isTransactional = $isTransactional;
            }
        }

        $runtime = $this;

        // Bootstrap the Context
        // + Runtime
        //   + StorageManager
        //     + FileSystem
        //     + Architecture

        $isTransactional = $this->isTransactional;

        $logFolder = $this->root . DS . 'log';
        $context = new Context(function(Binder $binder) use ($runtime, $isTransactional, $logFolder) {

            if($isTransactional) {
                $binder->bind('mg\Logger')->toInstance(new FileLogger('runtime', $logFolder));
            } else {
                $binder->bind('mg\Logger')->toInstance(new NullLogger('null'));
            }

            $binder->bind('mg\Runtime')->toInstance($runtime);
            $binder->bind('mg\Cache')->to(new CacheProvider())->in(Scope :: SINGLETON);

            $binder->bind('mg\StorageManager')->to('mg\StorageManager')->in(Scope :: SINGLETON);
            $binder->bind('mg\FileSystem')->to('mg\DefaultFileSystem')->in(Scope :: SINGLETON);
        });

        $this->isBootstrapped = true;

        if($this->isTransactional) {

            set_time_limit(0);

            $requiredExtensions = array('mbstring', 'intl');

            foreach($requiredExtensions as $index => $extension) {
                if(extension_loaded($extension)) {
                    unset($requiredExtensions[$index]);
                }
            }

            if(!empty($requiredExtensions)) {
                throw new Exception("Cannot initiate runtime without extension".(count($requiredExtensions) > 1 ? 's ' : ' ') . implode(',', $requiredExtensions));
            }

            $transactionals =& $this->transactionals;

            /*
             * If the Runtime is in transactional mode, embed a TransactionalContext in the wrapping
             * anonymous function in order to achieve transactional behaviour.
             */
            $wrappingClosure = function(Closure $closure) use($context, &$transactionals) {
                $nestedClosure = function(Context $context, Runtime $runtime, StorageManager $storageManager) use($closure, &$transactionals) {
                    $transactionals[] = $storageManager;

                    $context = new TransactionalContext($context);
                    $context->addTransactional($runtime);


                    $context($closure);
                };

                $context($nestedClosure);
            };

        } else {
            $wrappingClosure = function(Closure $closure) use($context) {
                $context($closure);
            };
        }


        $classSources = $this->classSources;

        $configuration = $this->runtimeConfiguration;

        if($configuration === null) {
            $configuration = new RuntimeConfiguration();
        }

        /*
         * Execute the wrapping closure, possibly initiating the transactional behaviour.
         */
        $wrappingClosure(function(Context $context) use($closure, $classSources, $configuration) {

            /*
             * Lift the Context with knowledge about the Architecture and the ClassLoader
             */
            $context = new Context($context, function(Binder $binder) use($classSources) {
                $binder->bind('mg\Architecture')->toInstance(new Architecture($classSources));
                $binder->bind('mg\ClassLoader')->to('mg\ClassLoader')->in(Scope :: SINGLETON);
            });

            /*
             * Set the default environmental properties such as timezone, error handler, class
             * loader etc. and execute the initial anonymous function.
             */
            $context(function(Context $context, ClassLoader $classLoader) use($closure, $configuration) {
                spl_autoload_register(array($classLoader, 'load'));

                // Register the exception handler
                set_error_handler(array ("mg\EngineException",	"errorHandler"), E_ALL);

                // Default to UTC timezone
                $timezone = @date_default_timezone_get();

                $timeout = ini_get('max_execution_time');

                if($timeout === false) {
                    $timeout = 30;
                }

                date_default_timezone_set('UTC');

                // Default to utf-8 encoding
                mb_internal_encoding(Charsets :: UTF_8);

                $context = new ConfiguredContext($context, $configuration);

                set_time_limit($timeout);

                // Execute the runtime
                $context($closure);

                // Restore previous state
                date_default_timezone_set($timezone);

                restore_error_handler();

                // reset display errors/ error reporting level

                spl_autoload_unregister(array($classLoader, 'load'));
            });
        });
    }

    protected function createFile($filename) {
        if(!file_exists($filename)) {
            $dirname = dirname($filename);
            mkdirrwx($dirname);

            touch($filename);
            chmod($filename, 0666);
        }
    }

    protected function lock() {
        $lFilename = $this->root . DS . 'volatile' . DS . 'lock.mg';

        $this->createFile($lFilename);

        // Acquire a mutex
        $this->lHandle = fopen($lFilename, 'w+');

        if($this->lHandle === false) {
            throw new Exception("Invalid runtime state : '{$lFilename}' is not writable");
        }

        // Acquire a lock or throw an exception when incapable of doing so
        if(!flock($this->lHandle, LOCK_EX)) {
            fclose($this->lHandle);
            throw new Exception("Invalid runtime state : '{$lFilename}' could not be locked");
        }

    }

    protected function readVersion() {
        $vFilename = $this->root . DS . 'volatile' . DS . 'version.mg';
        $this->createFile($vFilename);

        $version = file_get_contents($vFilename);

        if($version === '') {
            $version = 0;
        } else {
            $version = intval($version);
        }

        return $version;
    }

    protected function writeVersion($version) {
        $vFilename = $this->root . DS . 'volatile' . DS . 'version.mg';

        $this->createFile($vFilename);

        file_put_contents($vFilename, strval($version));

    }

    protected function unlock() {
        if($this->lHandle !== null) {
            flock($this->lHandle, LOCK_UN);
            fclose($this->lHandle);
            $this->lHandle = null;
        }
    }

    /**
     * Get the root folder of the Runtime
     *
     * @return string The root folder of the Runtime
     */
    public function getRoot() {
        return $this->root;
    }

    /**
     * Inform the Runtime of being in a transaction.
     *
     * @return void
     */
    public function begin() {
        if(!$this->isBootstrapped) {
            throw new Exception("Invalid runtime state : Inactive runtime");
        }

        foreach($this->transactionals as $transactional) {
            $transactional->begin();
        }
    }

    /**
     * Inform the Runtime of rolling back the transaction.
     *
     * @return void
     */
    public function rollback() {
        if(!$this->isBootstrapped) {
            throw new Exception("Invalid runtime state : Inactive runtime");
        }

        foreach($this->transactionals as $transactional) {
            $transactional->rollback();
        }
    }

    /**
     * If the Runtime does not run in development mode, committing
     * the Runtime results marking the Runtime as being deployed.
     *
     * If the Runtime is deployed, any future runs of the Runtime
     * will result in no attempts to deploy the Runtime again.
     *
     * @return void
     */
    public function commit() {
        if(!$this->isBootstrapped) {
            throw new Exception("Invalid runtime state : Inactive runtime");
        }

        /*
         * Lock, read version
         *                    -> increase version, commit, unlock
         *                    -> unlock, rollback
         */
        $this->lock();

        $version = $this->readVersion();

        if($version !== $this->version) {
            $this->unlock();
            $this->rollback();

            return;
        }

        $this->writeVersion($this->version + 1);

        foreach($this->transactionals as $transactional) {
            $transactional->commit();
        }

        $this->unlock();

        if($this->getMode() !== self :: MODE_DEVELOPMENT) {
            $dFilename = $this->root . DS . 'deployed.mg';
            touch($dFilename);
            chmod($dFilename, 0666);

            // Clear the cache for the next non transactional iteration
            in(function(Cache $cache = null) {
                if($cache === null) {
                    return;
                }

                $cache->clear();
            });
        }

    }

    /**
     * Check whether the Runtime is transactional.
     *
     * A transactional Runtime implies that any changes made
     * in the deployment process might not be committed after
     * the deployment phase has finished execution.
     *
     * @return boolean True if the Runtime is transactional
     */
    public function isTransactional() {
        return $this->isTransactional;
    }

    /**
     * Get a (probably) unique token for this Runtime. If a token
     * is not available, one will be generated and stored in the
     * Runtime root. Any future executions should return the
     * same token.
     *
     * @return string The unique token for the Runtime
     */
    public function getRuntimeToken() {
        if(!$this->isBootstrapped) {
            throw new Exception("Invalid runtime state : Inactive runtime");
        }

        if($this->token !== null) {
            return $this->token;
        }

        $tFilename = $this->root . DS . 'volatile' . DS . 'token.mg';

        if(!file_exists($tFilename)) {
            $tDirname = dirname($tFilename);

            mkdirrwx($tDirname);

            $this->token = md5(rand());

            file_put_contents($tFilename,"<?php return '{$this->token}';", FILE_TEXT);

            chmod($tFilename, 0666);

        } else {
            $this->token = include($tFilename);
        }

        return $this->token;
    }

    /**
     * Get exclusive rights to reading and writing contents in a volatile folder within the root directory.
     * When exclusive access can not be granted, an Exception is thrown. Otherwise the full path
     * to the folder is returned. Any extension writing temporary files into the root directory should
     * use this method for getting exclusive reading / writing access.
     *
     * @param string $name The name of the folder
     *
     * @throws \mg\Exception
     *
     * @return string The canonical path to the reserved, writable folder
     */
    public function getResource($name) {
        $name = (string) $name;

        $folder = md5($name);

        $folder = $this->root . DS . 'volatile' . DS . $folder;

        if($this->isTransactional) {
            if(!file_exists($folder)) {
                mkdirrwx($folder);
            }

            if(!is_dir($folder)) {
                throw new Exception("Cannot claim resource '{$name}', '{$folder}' is not a directory");
            }

            if(!is_writable($folder)) {
                throw new Exception("Cannot claim resource '{$name}', '{$folder}' is not a writable directory");
            }
        }

        return $folder;
    }

    /**
     * Return the operating system the environment is running in. This will be any of
     * {@link \mg\Runtime :: OS_WIN}, {@link \mg\Runtime :: OS_LINUX}, {@link \mg\Runtime :: OS_BSD} or {@link \mg\Runtime :: OS_UNKNOWN}.
     *
     * @return int
     */
    public function getOS() {
        if(self :: $os === null) {
            if(mb_stristr(PHP_OS, 'win')) {
                self :: $os = self :: OS_WIN;
            } elseif(mb_stristr(PHP_OS, 'linux')) {
                self :: $os = self :: OS_LINUX;
            } elseif(mb_stristr(PHP_OS, 'bsd')) {
                self :: $os = self :: OS_BSD;
            } else {
                self :: $os = self :: OS_UNKNOWN;
            }
        }
        return self :: $os;
    }
}

/**
 * An empty ContextConfiguration
 */
class RuntimeConfiguration implements ContextConfiguration {

    public function configure(Binder $binder) {

    }
}
